package com.gizwits.datapoint

import spray.json._
import scala.collection.immutable.BitSet
import scala.collection.mutable.ListBuffer

/**
 * @author basil
 */
case class DataPoint(name: String, productKey: String,
  protoType: String,
  packetVersion: String,
  entities: List[Entity])
{
  lazy val bytesOfFlags: Int = Math.ceil(numOf(DataAccess.Write) / 8.0).toInt
  lazy val bytesOfWritable: Int = bytesOf(DataAccess.Write)
  lazy val bytesOfReadonly: Int = bytesOf(DataAccess.Read)
  lazy val bytesOfAlert: Int = bytesOf(DataAccess.Alert)
  lazy val bytesOfFault: Int = bytesOf(DataAccess.Fault)

  def bytesOf(dataAccess: DataAccess.Value): Int = {
    val groupAttrs = entities(0).attrs.filter(_.dataAccess == dataAccess)
    val posLast = groupAttrs.last.position
    val posFirst = groupAttrs.head.position
    val length = if (posLast.unit == DataUnit.Byte) posLast.len else Math.ceil(posLast.len / 8.0)
    posLast.byteOffset + length.toInt - posFirst.byteOffset
  }

  def numOf(dataAccess: DataAccess.Value): Int = {
    entities(0).attrs.map(attr =>
        if (attr.dataAccess == dataAccess) 1 else 0
        ).sum
  }

  def getFlag(name: String): List[Byte] = {
    val index = entities(0).attrs.
      filter(_.dataAccess == DataAccess.Write).
      indexWhere(_.name == name)
    Bits.fromBitSet(BitSet(index), bytesOfFlags).toList
  }

  def findAttr(name: String) = {
    val found = entities(0).attrs.filter(_.name == name)
    if (found.length > 0) found(0) else null
  }

  def attrsOf(cmdType: CmdType): List[Attr] = {
    if (cmdType == CmdWrite) {
      entities(0).attrs.filter(_.dataAccess == DataAccess.Write)
    } else if (cmdType == CmdRead) null else {
      entities(0).attrs
    }
  }

  def buildData(cmdType: CmdType, values: Map[String, Any]): List[Byte] = {
    val attrs: List[Attr] = attrsOf(cmdType)
    val data = new ListBuffer[Byte]
    val bytesOfTarget = if (cmdType == CmdWrite) bytesOfWritable else (bytesOfWritable + bytesOfReadonly + bytesOfAlert + bytesOfFault)
    for (i <- 1 to bytesOfTarget) data += 0

    attrs.map(attr =>
      if (values.contains(attr.name)) {
        val value = values(attr.name)
        val pos = attr.position
        attr.dataType match {
          case DataType.Bool => {
            value match {
              case true => {
                val (byteOffset, bitOffset) = offsetOfBit(attr, attrs)
               data(byteOffset) = (data(byteOffset) | (1 << bitOffset)).toByte
              }
              case _ => ;
            }
          }
          case DataType.Enum => {
            value match {
              case str: String => {
                val iValue = attr.enums.get.indexOf(str).toByte
                val (byteOffset, bitOffset) = offsetOfBit(attr, attrs)
                fillEnumByte(data, byteOffset, bitOffset, pos.len, iValue)
              }
              case _ => ;
            }
          }
          case DataType.UInt8 => {
            value match {
              case y: Int => {
                val x = calcX(attr.uintSpec.get, y)
                fillInt(data, pos, x)
              }
              case _ => ;
            }
          }
          case DataType.UInt16 => {
            value match {
              case y: Int => {
                val x = calcX(attr.uintSpec.get, y)
                fillInt(data, pos, x)
              }
              case _ => ;
            }
          }
        }
      }
    )
    data.toList
  }

  def calcX(spec: UIntSpec, y: Int): Int = {
    (y - spec.mAddition.toInt) / spec.kRatio.toInt
  }

  def calcY(spec: UIntSpec, x: Int): Int = {
    spec.kRatio.toInt * x + spec.mAddition.toInt
  }

  def fillEnumByte(data: ListBuffer[Byte], byteOffset: Int, bitOffset: Int, len: Int, value: Byte) {
    if (bitOffset + len > 8) {
      // split into two parts
      val lenOfFirst = 8 - bitOffset
      val lenOfSecond = len - lenOfFirst
      val valueFirst = value & (0xFF >> (8 - lenOfFirst))
      val valueSecond = value >> lenOfFirst
      fillEnumByte(data, byteOffset, bitOffset, lenOfFirst, valueFirst.toByte)
      fillEnumByte(data, byteOffset - 1, 0, lenOfSecond, valueSecond.toByte)
    } else {
      data(byteOffset) = (data(byteOffset) | (value << bitOffset)).toByte
    }
  }

  def fillInt(data: ListBuffer[Byte], pos: Position, value: Int): Unit = {
    val bb = java.nio.ByteBuffer.allocate(pos.len)
    pos.len match {
      case 1 => bb.put(value.toByte)
      case 2 => bb.putShort(value.toShort)
      case 4 => bb.putInt(value)
    }
    val arr = bb.array
    for (i <- 0 to (pos.len - 1)) {
      data(pos.byteOffset + i) = arr(i)
    }
  }

  def offsetOfBit(attr: Attr, group: List[Attr]) = {
    val pos = attr.position
    val boolAttrs = group.partition(_attr =>
      attr.dataAccess == _attr.dataAccess && (_attr.dataType == DataType.Bool || _attr.dataType == DataType.Enum)
    )._1
    val lengthOfBits = boolAttrs.map(_.position.len).sum
    val bytesOfBits = Math.ceil(lengthOfBits / 8.0).toInt
    val bitOffset = pos.bitOffset % 8
    val byteOffset = pos.byteOffset + bytesOfBits - (pos.bitOffset / 8) - 1
    (byteOffset, bitOffset)
  }

  def read(): List[Byte] = List[Byte](CmdRead.value)

  def write(name: String, value: Any): List[Byte] = {
    CmdWrite.value :: getFlag(name) ::: buildData(CmdWrite, Map(name -> value))
  }

  def response(values: Map[String, Any]): List[Byte] = {
    CmdResponse.value :: buildData(CmdResponse, values)
  }

   def notify(values: Map[String, Any]): List[Byte] = {
    CmdNotify.value :: buildData(CmdNotify, values)
  }

  def parse(packet: List[Byte]) = {
    val cmd = packet.head
    val bytes = packet.tail
    val cmdType = CmdType(cmd)

    val attrs = entities(0).attrs
    val values = attrs.map(attr => {
        val pos = attr.position
        attr.dataType match {
          case DataType.Bool => {
            val (byteOffset, bitOffset) = offsetOfBit(attr, attrs)
            val value = if (byteOffset < bytes.length) {
              (bytes(byteOffset) & (1 << bitOffset)) != 0
            } else false
            (attr.name, value)
          }
          case DataType.Enum => {
            val (byteOffset, bitOffset) = offsetOfBit(attr, attrs)
            val index = if (bitOffset + pos.len > 8) {
              0 // fixme
            } else {
              (bytes(byteOffset) & (0xFF >> (8 - pos.len) << bitOffset)) >> bitOffset
            }
            val value = if (index < attr.enums.get.length) {
              (attr.enums.get)(index)
            } else null
            (attr.name, value)
          }
          case DataType.UInt8 => {
            val value = bytes(pos.byteOffset).toInt
            (attr.name, calcY(attr.uintSpec.get, value))
          }
          case DataType.UInt16 => {
            val value = java.nio.ByteBuffer.wrap(bytes.slice(pos.byteOffset, pos.byteOffset + pos.len).toArray).getShort
            (attr.name, calcY(attr.uintSpec.get, value))
          }
          case DataType.UInt32 => {
            val value = java.nio.ByteBuffer.wrap(bytes.slice(pos.byteOffset, pos.byteOffset + pos.len).toArray).getInt
            (attr.name, calcY(attr.uintSpec.get, value))
          }
          case _ => (attr.name, null)
        }
    }).toMap
    (cmdType, values)
  }
}

object DataAccess extends Enumeration {
  val Read=Value("status_readonly")
  val Write=Value("status_writable")
  val Alert=Value("alert")
  val Fault = Value("fault")
}

object DataType extends Enumeration {
  val Bool = Value("bool")
  val Enum = Value("enum")
  val UInt8 = Value("uint8")
  val UInt16 = Value("uint16")
  val UInt32 = Value("uint32")
}

sealed trait CmdType { var value = 0.toByte }
case object CmdWrite extends CmdType { value = 1.toByte }
case object CmdRead extends CmdType { value = 2.toByte }
case object CmdResponse extends CmdType { value = 3.toByte }
case object CmdNotify extends CmdType { value = 4.toByte }
object CmdType {
  def apply(value: Byte) = {
    if (value == CmdWrite.value) CmdWrite
    else if (value == CmdRead.value) CmdRead
    else if (value == CmdResponse.value) CmdResponse
    else CmdNotify
  }
}

object DataUnit extends Enumeration {
  val Bit=Value("bit")
  val Byte = Value("byte")
}

case class Entity(id: Int, name: String, displayName: String, attrs: List[Attr])

case class UIntSpec(mAddition: Float, xMax: Int, xMin: Int, kRatio: Float)

case class Position(bitOffset: Int, byteOffset: Int, unit: DataUnit.Value, len: Int)

case class Attr(id: Int, name: String, displayName: String, description: String, dataAccess: DataAccess.Value, dataType: DataType.Value, position: Position, enums: Option[List[String]], uintSpec: Option[UIntSpec])

object DataPointJsonProtocol extends DefaultJsonProtocol with NullOptions {
  def jsonEnum[T <: Enumeration](enu: T) = new JsonFormat[T#Value] {
    def write(obj: T#Value) = JsString(obj.toString)

    def read(json: JsValue) = json match {
      case JsString(txt) =>
        try {
          enu.withName(txt)
        } catch {
          case e : NoSuchElementException => deserializationError(s"Expected a value from enum $enu instead of $txt", e)
        }
      case something => deserializationError(s"Expected a value from enum $enu instead of $something")
    }
  }
  implicit val dataUnitFormat = jsonEnum(DataUnit)
  implicit val dataTypeFormat = jsonEnum(DataType)
  implicit val dataAccessFormat = jsonEnum(DataAccess)
  implicit val positionFormat = jsonFormat(Position.apply, "bit_offset", "byte_offset", "unit", "len")
  implicit val uintSpec = jsonFormat(UIntSpec.apply, "addition", "max", "min", "ratio")
  implicit val attrFormat = jsonFormat(Attr.apply, "id", "name", "display_name", "desc", "type", "data_type", "position", "enum", "uint_spec")
  implicit val entityFormat = jsonFormat(Entity.apply, "id", "name", "display_name", "attrs")

  implicit val DataPointFormat: RootJsonFormat[DataPoint] = jsonFormat(DataPoint.apply, "name", "product_key", "protocolType", "packetVersion", "entities")
}

object DataPoint {
  def apply(jsonStr: String): DataPoint = {
    val json = jsonStr.parseJson
    import DataPointJsonProtocol._
    val dp = json.convertTo[DataPoint]
    dp
  }
}
